package ru.housewife.form;

import org.springframework.validation.Errors;
import ru.housewife.model.DayMenu;
import ru.housewife.model.UsersTable;
import ru.housewife.utils.TimeUtils;

import java.util.*;

/**
 * Form object for
 * @see ru.housewife.model.UsersTable
 *
 * @author Alexander Troshanin
 */
public class UsersTableForm extends AbstractFormObject<UsersTableForm> implements Comparable<UsersTableForm> {

    private long id;

    private String name;

    private UsersTable.Type type = UsersTable.Type.WEEK_7;

    private boolean permanent;

    private Date from;

    private Date to;

    private int periodLength;

    private Map<Long, DayMenuForm> dayMenus;

    private List<DayMenuForm> dayMenusSortedCache;

    private DayMenuForm [] dayMenusByDay;

    private UserForm user;

    public UsersTableForm() {
    }

    public UsersTableForm(UserForm user, UsersTable ut) {
        this.user = user;
        id = ut.getId();
        name = ut.getName();
        type = ut.getType();
        permanent = ut.isPermanent();
        from = ut.getFrom();
        to = ut.getTo();
        periodLength = ut.getPeriodLength();
        dayMenus = new HashMap<Long, DayMenuForm>();
        for (DayMenu dm : ut.getDayMenus()) {
            DayMenuForm dmf = new DayMenuForm(this, dm);
            dmf.setUsersTable(this);
            dayMenus.put(dmf.getId(), dmf);
        }
        dayMenusSortedCache = generateSortedCache(dayMenus);
        dayMenusByDay = generateDayMenusByDayArray(dayMenusSortedCache);

//        Map<Integer, List<DayMenuForm>> test = getMap();
//        System.out.println(test);
    }

    private DayMenuForm [] generateDayMenusByDayArray(List<DayMenuForm> lst) {
        DayMenuForm [] result = new DayMenuForm[periodLength];
        for (DayMenuForm dmf : lst) {
            int num = dmf.getDayNumber();
            if (num >= 0 && num < periodLength) {
                result[num] = dmf;
            }
        }
        return result;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public UsersTable.Type getType() {
        return type;
    }

    public void setType(UsersTable.Type type) {
        this.type = type;
    }

    public boolean isShowTypeName() {
        return type.getPeriodLength() > 0;
    }

    public boolean isPermanent() {
        return permanent;
    }

    public void setPermanent(boolean permanent) {
        this.permanent = permanent;
    }

    public Date getFrom() {
        return from;
    }

    public void setFrom(Date from) {
        this.from = from;
    }

    public Date getTo() {
        return to;
    }

    public void setTo(Date to) {
        this.to = to;
    }

    public int getPeriodLength() {
        return periodLength;
    }

    public void setPeriodLength(int periodLength) {
        this.periodLength = periodLength;
    }

    public List<DayMenuForm> getDayMenus() {
        return Collections.unmodifiableList(dayMenusSortedCache);
    }

    public DayMenuForm[] getDayMenusByDay() {
        return dayMenusByDay;
    }

    public DayMenuForm getDayMenu(long dayMenuId) {
        return dayMenus.get(dayMenuId);
    }

    public void addDayMenu(DayMenuForm dayMenuForm) {
        dayMenuForm.setUsersTable(this);
        dayMenus.put(dayMenuForm.getId(), dayMenuForm);
        generateSortedCache(dayMenusSortedCache, dayMenus);
        dayMenusByDay = generateDayMenusByDayArray(dayMenusSortedCache);
    }

    public void removeDayMenu(long dayMenuId) {
        dayMenus.remove(dayMenuId);
        generateSortedCache(dayMenusSortedCache, dayMenus);
        dayMenusByDay = generateDayMenusByDayArray(dayMenusSortedCache);
        user.regenerateUsersWares();
    }

    public UserForm getUser() {
        return user;
    }

    public void setUser(UserForm user) {
        this.user = user;
    }

    public void validte(Errors errors) {
        if (periodLength <= 0) {
            errors.rejectValue("periodLength", "periodLength.bad.value");
        }
        //Todo implement it
    }

    public boolean isActive() {
        long time = System.currentTimeMillis();
        return from.getTime() < time && time < to.getTime();
    }

    public boolean isWeekBased() {
        return type == UsersTable.Type.WEEK_7;
    }

    @Override
    public int compareTo(UsersTableForm that) {
        boolean thisActive = this.isActive();
        boolean thatActive = that.isActive();
        if (thisActive == thatActive) {
            long thisId = this.id;
            long thatId = that.id;
            return -(thisId < thatId ? -1 : (thisId == thatId ? 0 : 1));
        }
        return thisActive ? 1 : -1;
    }

    @Override
    public void updateFieldsFromSrcForm(UsersTableForm srcForm) {
        this.name = srcForm.name;
        this.type = srcForm.type;
        this.permanent = srcForm.permanent;
        this.from = srcForm.from;
        this.to = srcForm.to;
        this.periodLength = srcForm.periodLength;
    }

    public UsersTableFullBlockForm getUsersTableBlockForm() {
        return getUsersTableBlockForm(null);
    }

    public UsersTableFullBlockForm getUsersTableBlockForm(Date time) {
        // todo: CREATE UNIT TESTS FOR THIS METHOD
        Calendar c = Calendar.getInstance();
        if (time != null) {
            c.setTime(time);
        }

        // TODO: fix problem with curDayPeriod - currently it may work only with N_days_period
        int curDayOfPeriod;
        switch (type) {
            case WEEK_7: {
                curDayOfPeriod = c.get(Calendar.DAY_OF_WEEK);
                curDayOfPeriod = (curDayOfPeriod == Calendar.SUNDAY) ? 7 : curDayOfPeriod - 1;
            } break;
            case MONTH_31: {
                curDayOfPeriod = c.get(Calendar.DAY_OF_MONTH);
                curDayOfPeriod = (curDayOfPeriod == Calendar.SUNDAY) ? 7 : curDayOfPeriod - 1;
            } break;
            case N_DAYS_PERIOD: {
                int daysCntFromStart;
                if (time != null) {
                    daysCntFromStart = TimeUtils.getDaysCountBetweenDates(from, time);
                } else {
                    daysCntFromStart = TimeUtils.getDaysCountBetweenDateAndNow(from);
                }
                curDayOfPeriod = daysCntFromStart % periodLength;
                curDayOfPeriod = (curDayOfPeriod == 0) ? periodLength : curDayOfPeriod;
            } break;
            default:
                throw new RuntimeException("unknown type");
        }

        c.add(Calendar.DATE, -curDayOfPeriod + 1);      // 1. shifting to first period day.
        int dayOfMonth = c.get(Calendar.DAY_OF_MONTH);  // 2. getting day of month number of first period day

        List<LinkedList<DayMenuWebWrapperForm>> months = new ArrayList<LinkedList<DayMenuWebWrapperForm>>();
        LinkedList<DayMenuWebWrapperForm> curMonth = new LinkedList<DayMenuWebWrapperForm>();
        int dayNum = periodLength - 1;
        for (int i=1; i<dayOfMonth; i++) {
            c.add(Calendar.DATE, -1);
            curMonth.addFirst(new DayMenuWebWrapperForm(this, dayMenusByDay[dayNum], dayNum, false, false, c));
            dayNum--;
            dayNum = (dayNum < 0) ? periodLength - 1 : dayNum;
        }
        c.add(Calendar.DATE, dayOfMonth - 1);

        boolean isCurPeriod = true;
        dayNum = 0;
        int currentMonthIndex = 0;
        while (isCurPeriod) {
            int lastDayInMonth = c.getActualMaximum(Calendar.DAY_OF_MONTH);
            for (int i = dayOfMonth; i<=lastDayInMonth; i++) {
                boolean today = (isCurPeriod && dayNum == (curDayOfPeriod - 1));
                curMonth.add(new DayMenuWebWrapperForm(this, dayMenusByDay[dayNum], dayNum, isCurPeriod, today, c));
                dayNum++;
                if ((type != UsersTable.Type.MONTH_31 && dayNum >= periodLength) || (type == UsersTable.Type.MONTH_31 && dayNum >= lastDayInMonth)) {
                    isCurPeriod = false;
                    dayNum = 0;
                }
                c.add(Calendar.DATE, 1);
                if (today) {
                    currentMonthIndex = months.size() + 1;
                }
            }
            months.add(curMonth);
            curMonth = new LinkedList<DayMenuWebWrapperForm>();
            dayOfMonth = 1;
        }

        List<MonthWrapperForm> result0 = new ArrayList<MonthWrapperForm>(months.size());
        for (LinkedList<DayMenuWebWrapperForm> daysInMonth : months) {
            DayMenuWebWrapperForm firstDay = daysInMonth.getFirst();
            DayMenuWebWrapperForm lastDay = daysInMonth.getLast();
            MonthWrapperForm month = new MonthWrapperForm(firstDay.getDate());

            int dayOfWeek = firstDay.getDayOfWeek();
            while (dayOfWeek != Calendar.MONDAY) {
                daysInMonth.addFirst(null);
                dayOfWeek = (dayOfWeek == Calendar.SUNDAY) ? Calendar.SATURDAY : dayOfWeek - 1;
            }
            dayOfWeek = lastDay.getDayOfWeek();
            while (dayOfWeek != Calendar.SUNDAY) {
                daysInMonth.addLast(null);
                dayOfWeek = (dayOfWeek == Calendar.SATURDAY) ? Calendar.SUNDAY : dayOfWeek + 1;
            }

            int weekNum = 0;
            dayNum = 0;
            WeekWrapperForm week = new WeekWrapperForm(++weekNum);
            for (DayMenuWebWrapperForm day : daysInMonth) {
                week.addDay(day);
                dayNum++;
                if (dayNum % 7 == 0) {
                    month.addWeek(week);
                    week = new WeekWrapperForm(++weekNum);
                }
            }
            result0.add(month);
        }
        return new UsersTableFullBlockForm(this, result0, currentMonthIndex);
    }

    public UsersTableBriefBlockForm getUsersTableBriefBlockForm() {
        // todo: CREATE UNIT TESTS FOR THIS METHOD
        UsersTableBriefBlockForm result = new UsersTableBriefBlockForm(this);
        List<DayMenuWebWrapperForm> dayMenuWebWrappers = new ArrayList<DayMenuWebWrapperForm>(periodLength + 10);
        result.setDayMenuWebWrappers(dayMenuWebWrappers);
        switch (type) {
            case WEEK_7: {
                Calendar c = Calendar.getInstance();
                int curDayOfWeek = c.get(Calendar.DAY_OF_WEEK) - 2;
                curDayOfWeek = (curDayOfWeek < 0) ? 6 : curDayOfWeek;
                int addDaysLeftCnt = 3 - curDayOfWeek;
                if (addDaysLeftCnt > 0) {
                    c.add(Calendar.DAY_OF_MONTH, -3);
                    for (int i = addDaysLeftCnt; i > 0; i--) {
                        int dayNum = 7 - i;
                        dayMenuWebWrappers.add(new DayMenuWebWrapperForm(this, dayMenusByDay[dayNum], dayNum, false, c));
                        c.add(Calendar.DAY_OF_MONTH, 1);
                    }
                    result.setTodayIndex(curDayOfWeek + addDaysLeftCnt);
                } else {
                    result.setTodayIndex(curDayOfWeek);
                    c.add(Calendar.DAY_OF_MONTH, -curDayOfWeek);
                }

                for (int i=0; i<dayMenusByDay.length; i++) {
                    dayMenuWebWrappers.add(new DayMenuWebWrapperForm(this, dayMenusByDay[i], i, true, i == curDayOfWeek, c));
                    c.add(Calendar.DAY_OF_MONTH, 1);
                }

                int addDaysRightCnt = curDayOfWeek - 3;
                if (addDaysRightCnt > 0) {
                    for (int i = 0; i < addDaysRightCnt; i++) {
                        dayMenuWebWrappers.add(new DayMenuWebWrapperForm(this, dayMenusByDay[i], i, false, c));
                        c.add(Calendar.DAY_OF_MONTH, 1);
                    }
                }
            } break;

            case MONTH_31: {
                Calendar c = Calendar.getInstance();
                int curDayOfMonth = c.get(Calendar.DAY_OF_MONTH) - 1;
                int addDaysLeftCnt = 3 - curDayOfMonth;
                if (addDaysLeftCnt > 0) {
                    c.add(Calendar.DAY_OF_MONTH, -3);
                    for (int i = addDaysLeftCnt; i > 0; i--) {
                        int dayNum = c.get(Calendar.DAY_OF_MONTH) - 1;
                        dayMenuWebWrappers.add(new DayMenuWebWrapperForm(this, dayMenusByDay[dayNum], dayNum, false, c));
                        c.add(Calendar.DAY_OF_MONTH, 1);
                    }
                    result.setTodayIndex(curDayOfMonth + addDaysLeftCnt);
                } else {
                    result.setTodayIndex(curDayOfMonth);
                    c.add(Calendar.DAY_OF_MONTH, -curDayOfMonth);
                }

                for (int i=0; i<dayMenusByDay.length; i++) {
                    dayMenuWebWrappers.add(new DayMenuWebWrapperForm(this, dayMenusByDay[i], i, true, i == curDayOfMonth, c));
                    c.add(Calendar.DAY_OF_MONTH, 1);
                }


                int addDaysRightCnt = curDayOfMonth - c.getActualMaximum(Calendar.DAY_OF_MONTH) + 3;
                if (addDaysRightCnt > 0) {
                    for (int i = 0; i < addDaysRightCnt; i++) {
                        dayMenuWebWrappers.add(new DayMenuWebWrapperForm(this, dayMenusByDay[i], i, false, c));
                        c.add(Calendar.DAY_OF_MONTH, 1);
                    }
                }
            } break;
            case N_DAYS_PERIOD: {
                // TODO: check here id periodLength is less than 3
                Calendar c = Calendar.getInstance();
                int daysCntFromStart = TimeUtils.getDaysCountBetweenDateAndNow(from);

                int curDayOfPeriod = daysCntFromStart % periodLength;

                int addDaysLeftCnt = 3 - curDayOfPeriod;
                if (addDaysLeftCnt > 0) {
                    c.add(Calendar.DAY_OF_MONTH, -3);
                    for (int i = addDaysLeftCnt; i > 0; i--) {
                        int dayNum = periodLength - i;
                        dayMenuWebWrappers.add(new DayMenuWebWrapperForm(this, dayMenusByDay[dayNum], dayNum, false, c));
                        c.add(Calendar.DAY_OF_MONTH, 1);
                    }
                    result.setTodayIndex(curDayOfPeriod + addDaysLeftCnt);
                } else {
                    result.setTodayIndex(curDayOfPeriod);
                    c.add(Calendar.DAY_OF_MONTH, -curDayOfPeriod);
                }

                for (int i=0; i<dayMenusByDay.length; i++) {
                    dayMenuWebWrappers.add(new DayMenuWebWrapperForm(this, dayMenusByDay[i], i, true, i == curDayOfPeriod, c));
                    c.add(Calendar.DAY_OF_MONTH, 1);
                }
                int addDaysRightCnt = curDayOfPeriod - periodLength + 3;
                if (addDaysRightCnt > 0) {
                    for (int i = 0; i < addDaysRightCnt; i++) {
                        dayMenuWebWrappers.add(new DayMenuWebWrapperForm(this, dayMenusByDay[i], i, false, c));
                        c.add(Calendar.DAY_OF_MONTH, 1);
                    }
                }

            }
        }

        return result;
    }

    /**
     * Methid returns map where keys are day numbers and values are list of daymenus associated with this day number
     * Actually map will contain exact number of key-values - number equals to periodLength.
     * So if for some day there is no daymenus than for such key, value will be NULL.
     * Algorith is simple:
     *  - iterate over dayMenusSortedCache (this list is sorted)
     *    Take current daymenu and determine dayNum(key) according to it.
     *    If dayNum(key) is less than dayMenuForm.getDayNumber(), then we add in result map nulls for dayNum and inc dayNum
     *    ...
     * @return map
     */
    public Map<Integer, List<DayMenuForm>> getMap() {
        int dayNum = 1;
        Map<Integer, List<DayMenuForm>> map = new TreeMap<Integer, List<DayMenuForm>>();
        int lastDayMenuDayNum = -1;
        for (DayMenuForm dayMenuForm : dayMenusSortedCache) {
            dayNum = (lastDayMenuDayNum != -1 && lastDayMenuDayNum < dayMenuForm.getDayNumber()) ? (dayNum + 1) : dayNum;
            lastDayMenuDayNum = dayMenuForm.getDayNumber();
            if (dayNum < dayMenuForm.getDayNumber()) {
                for (; dayNum < dayMenuForm.getDayNumber() && dayNum <= periodLength; dayNum++) {
                    map.put(dayNum, null);
                }
            }
            if (dayNum == dayMenuForm.getDayNumber()) {
                List<DayMenuForm> lst = map.get(dayNum);
                if (lst == null) {
                    lst = new LinkedList<DayMenuForm>();
                    map.put(dayNum, lst);
                }
                lst.add(dayMenuForm);
            }
            if (dayNum > periodLength) {
                break;
            }
        }
        dayNum = (lastDayMenuDayNum != -1) ? (dayNum + 1) : dayNum;
        for (; dayNum <= periodLength; dayNum++) {
            map.put(dayNum, null);
        }
        return map;
    }

}
